package qj.tool.gae;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import qj.tool.gae.db.AEDB;
import qj.tool.gae.db.AEDB.GaeDataConverter;
import qj.util.Cols;
import qj.util.funct.F0;
import qj.util.funct.F1;
import qj.util.funct.F2;
import qj.util.funct.P1;

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.FetchOptions.Builder;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;

@SuppressWarnings({"unchecked","rawtypes"})
public class DB {
	public static DatastoreService ds = DatastoreServiceFactory
			.getDatastoreService();
	public static AsyncDatastoreService ads = DatastoreServiceFactory
	.getAsyncDatastoreService();

	public static HashMap<Class<?>, GaeDataConverter> gaeConverters = new HashMap<Class<?>, GaeDataConverter>();

	public static void deleteAll(Class<?> clazz) {
		deleteAll(query(clazz));
	}

	public static void deleteAllAsync(Query q) {
		q.setKeysOnly();
		ads.delete(toKeys(ads.prepare(q).asIterable()));
	}

	public static void deleteAll(Query q) {
		q.setKeysOnly();
		ds.delete(toKeys(ds.prepare(q).asIterable()));
//		ds.delete(toKeys(ds.prepare(q).asList(FetchOptions.Builder.withDefaults())));
	}
	public static void deleteAllNice(Query q) {
		q.setKeysOnly();
		ds.delete(filter(toKeys(ds.prepare(q).asIterable()), new F1<Key, Boolean>() {public Boolean e(Key key) {
			return !key.getKind().startsWith("_");
		}}));
	}

	private static <A> Iterable<A> filter(final Iterable<A> iterA, final F1<A,Boolean> filter) {
		return new Iterable<A>() {
			public Iterator<A> iterator() {
				final Iterator<A> iterator = iterA.iterator();
				return new Iterator<A>() {
					A cache = null;
					public boolean hasNext() {
						if (cache!= null) {
							return true;
						} else {
							return cache(filter);
						}
					}

					private boolean cache(final F1<A, Boolean> filter) {
						while (iterator.hasNext()) {
							if (filter.e((cache = iterator.next()))) {
								return true;
							} else {
								continue;
							}
						}
						return false;
					}

					@Override
					public A next() {
						A a = cache;
						if (a!= null) {
							cache = null;
							return a;
						} else {
							cache(filter);
							return cache;
						}
					}

					@Override
					public void remove() {
						iterator.remove();
					}
				};
			}
		};
	}

	public static Iterable<Key> toKeys(final Iterable<Entity> entities) {
		return Cols.convert(entities, new F1<Entity, Key>() {public Key e(Entity e) {
			return e.getKey();
		}});
	}
	
	public static List<Long> getLongIds(Query query) {
		query.setKeysOnly();
		LinkedList<Long> ids = new LinkedList<Long>();
		for (Entity entity : ds.prepare(query).asIterable()) {
			ids.add(entity.getKey().getId());
		}
		return ids;
	}
	public static List<Key> getKeys(Query query) {
		return getKeys(query, null);
	}

	public static List<Key> getKeys(Query query, FetchOptions fetchOptions) {
		query.setKeysOnly();
		LinkedList<Key> ids = new LinkedList<Key>();
		Iterable<Entity> iterable = fetchOptions==null ? ds.prepare(query).asIterable() : ds.prepare(query).asIterable(fetchOptions);
		
		for (Entity entity : iterable) {
			ids.add(entity.getKey());
		}
		return ids;
	}
	
	public static List<String> getNames(Query query) {
		query.setKeysOnly();
		LinkedList<String> ids = new LinkedList<String>();
		for (Entity entity : ds.prepare(query).asIterable()) {
			ids.add(entity.getKey().getName());
		}
		return ids;
	}
	
	public static <A> void delete(A o) {
		if (o==null) {
			return;
		}
		GaeDataConverter gaeData = gaeConverter(o.getClass());
		if (gaeData.isNamedId()) {
			delete(gaeData.getNameId(o), entityType(o.getClass()));
		} else if (gaeData.isId()) {
			delete(gaeData.getId(o), entityType(o.getClass()));
		} else {
			ds.delete(gaeData.getKey(o));
		}
	}
	public static void delete(String id,String kind) {
		if (id==null) {
			return;
		}
		ds.delete(KeyFactory.createKey(kind, id));
	}

	public static void delete(Long id,String kind) {
		if (id==null) {
			return;
		}
		ds.delete(KeyFactory.createKey(kind, id));
	}

	public static <A> List<A> mapAll(Query q, Class<A> clazz) {
		return Cols.yield(ds.prepare(q).asList(FetchOptions.Builder.withDefaults()), mapF(clazz));
	}
	
	public static <A> List<A> retrieveLimit(Query q, int limit, Class<A> clazz) {
		return Cols.yield(ds.prepare(q).asList(FetchOptions.Builder.withLimit(limit)), mapF(clazz));
	}

	/**
	 * 
	 * @param <A>
	 * @param clazz
	 * @param equals field value
	 * @return
	 */
	public static <A> A get(Class<A> clazz,Object... equals) {
		Query query = query(clazz, equals);
		
		return get1(query, clazz);
	}

	public static Key getKey(Class<?> clazz,Object... equals) {
		Query query = query(clazz, equals);
		
		return getKey(query, clazz);
	}

	public static Key getKey(Class<?> clazz, Key ancestorKey, Object... equals) {
		Query query = query(clazz, ancestorKey, equals);
		
		return getKey(query, clazz);
	}

	public static <A> boolean exists(Class<A> clazz,Object... equals) {
		Query query = query(clazz, equals);

		query.setKeysOnly();
		List<Entity> list = ds.prepare(query).asList(Builder.withLimit(1));
		return !Cols.isEmpty(list);
	}

	public static Key getKey(Query query, Class<?> clazz) {
		query.setKeysOnly();
		List<Entity> list = ds.prepare(query).asList(Builder.withLimit(1));
		return Cols.isEmpty(list) ? null : list.get(0).getKey();
	}

	public static <A> A get1(Query query, Class<A> clazz) {
		List<Entity> list = ds.prepare(query).asList(Builder.withLimit(1));
		return Cols.isEmpty(list) ? null : map(list.get(0), clazz);
	}
	public static <A> Query query(Class<A> clazz, Object... equals) {
		Query query = query(clazz);
		setConds(query, equals);
		return query;
	}
	public static <A> Query query(Class<A> clazz, Key ancestorKey, Object... equals) {
		Query query = query(clazz, ancestorKey);
		setConds(query, equals);
		return query;
	}

	public static <A> Query queryK(Class<A> clazz, Key parentKey, Object... equals) {
		Query query = queryByKey(clazz, parentKey);
		setConds(query, equals);
		return query;
	}

	public static Query queryByKey(Class<?> clazz, Key key) {
		if (key==null) {
			return null;
		}
		return new Query(entityType(clazz), key);
	}

	public static <A> Query query(Class<A> clazz) {
		return new Query(entityType(clazz));
	}
	public static <A> Query query(Class<A> clazz, Key ancestor) {
		return new Query(entityType(clazz), ancestor);
	}

	public static <A> String entityType(Class<A> clazz) {
		return clazz.getSimpleName();
	}

	public static <A> List<A> getAll(Class<A> clazz,Object... equals) {
		Query query = query(clazz);
		setConds(query, equals);
		return getList(query, clazz);
	}
	public static <A> List<A> getAll(Key ancestorKey, Class<A> clazz,Object... equals) {
		Query query = query(clazz, ancestorKey);
		setConds(query, equals);
		return getList(query, clazz);
	}
	public static <A> List<Key> getAllKeys(Class<A> clazz,Object... equals) {
		Query query = query(clazz);
		setConds(query, equals);
		return getKeys(query);
	}

	private static void setConds(Query query, Object... equals) {
		for (int i = 0; i < equals.length; i+=2) {
			String field = (String) equals[i];
			Object value = equals[i+1];
			query.addFilter(field, FilterOperator.EQUAL, value);
		}
	}

	protected static <A> F2<Integer,Integer,List<A>> getAllF(final Class<A> clazz,final Object... equals) {
		return new F2<Integer,Integer,List<A>>() {public List<A> e(Integer from,Integer limit) {
			return DB.getAll(from, limit, clazz, equals);
		}};
	}

	public static <A> F0<Integer> countF(final Class<A> clazz,final Object... equals) {
		return new F0<Integer>() {public Integer e() {
			return DB.count(clazz, equals);
		}};
	}
	
	protected static <A> List<A> getAll(int from, int limit, Class<A> clazz,Object... equals) {
		Query query = query(clazz);
		setConds(query, equals);
		return getList(from,limit,query, clazz);
	}

	public static <A> List<A> getList(Query query, Class<A> clazz) {
		FetchOptions fetchOptions = Builder.withDefaults();
		return getList(query, clazz, fetchOptions);
	}

	public static <A> List<A> getList(Query query, Class<A> clazz,
			FetchOptions fetchOptions) {
		List<Entity> list = ds.prepare(query).asList(fetchOptions);
		return Cols.yield(list, mapF(clazz));
	}

	public static <A> List<A> getList(int from, int limit, Query query, Class<A> clazz) {
		FetchOptions fetchOptions = Builder.withDefaults();
		if (limit > -1) {
			fetchOptions.limit(limit);
		}
		if (from > 0) {
			fetchOptions.offset(from);
		}
		List<Entity> list = ds.prepare(query).asList(fetchOptions);
		return Cols.yield(list, mapF(clazz));
	}

	public static <A> F0<List<A>> getListAsync(int from, int limit, Query query, final Class<A> clazz) {
		FetchOptions fetchOptions = Builder.withDefaults();
		if (limit > -1) {
			fetchOptions.limit(limit);
		}
		if (from > 0) {
			fetchOptions.offset(from);
		}
		final List<Key> keys = getKeys(query, fetchOptions);
		final Future<Map<Key, Entity>> future = ads.get(keys);
		
		return new F0<List<A>>() {public List<A> e() {
			try {
				Map<Key, Entity> map = future.get();
				ArrayList<A> ret = new ArrayList<A>(keys.size());
				for (Key key : keys) {
					ret.add(map(map.get(key), clazz));
				}
				return ret;
			} catch (InterruptedException e1) {
				throw new RuntimeException(e1);
			} catch (ExecutionException e1) {
				throw new RuntimeException(e1);
			}
		}};
	}

	public static <A> Iterable<A> getIterAsync(Query query, final Class<A> clazz) {
		return getIterAsync(0, -1, query, clazz);
	}

	public static <A> Iterable<A> getIterAsync(int from, int limit, Query query, final Class<A> clazz) {
		FetchOptions fetchOptions = Builder.withDefaults();
		if (limit > -1) {
			fetchOptions.limit(limit);
		}
		if (from > 0) {
			fetchOptions.offset(from);
		}
		final List<Key> keys = getKeys(query);
		final Future<Map<Key, Entity>> future = ads.get(keys);
		
		return new Iterable<A>() {public Iterator<A> iterator() {
			try {
				final Map<Key, Entity> map = future.get();
				final Iterator<Key> keysIter = keys.iterator();
				return new Iterator<A>() {
					public boolean hasNext() {
						return keysIter.hasNext();
					}
					public A next() {
						return map(map.get(keysIter.next()), clazz);
					}
					public void remove() {
					}
				};
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			} catch (ExecutionException e) {
				throw new RuntimeException(e);
			}
		}};
		
	}

	public static <A> int count(Class<A> clazz,Object... equals) {
		Query query = query(clazz);
		setConds(query, equals);
		return count(query);
	}

	public static int count(Query query) {
		query.setKeysOnly();
		return ds.prepare(query).countEntities(FetchOptions.Builder.withDefaults());
	}
	public static <A> void deleteAll(Class<A> clazz,Key parentKey) {
		deleteAll(queryByKey(clazz, parentKey));
	}
	public static <A> void deleteAll(Class<A> clazz,Object... equals) {
		Query query = query(clazz);
		setConds(query, equals);
		deleteAll(query);
	}


	public static void saveAsync(Object obj) {
		ads.put(toEntity(obj));
	}
	
	public static void save(Object o) {
		Entity entity = toEntity(o, entityType(o.getClass()));
		ds.put(entity);

		GaeDataConverter gaeData = gaeConverter(o.getClass());
		
		Key key = entity.getKey();
		if (gaeData.isNamedId()) {
		} else {
			gaeData.setId(gaeData.isId() ? key.getId() : key, o);
		}
	}
	
	public static Entity toEntity(Object o) {
		return toEntity(o, entityType(o.getClass()));
	}
	public static Entity toEntity(Object obj, String kind) {
		
		GaeDataConverter gaeData = gaeConverter(obj.getClass());

		Entity entity;
		if (gaeData.isNamedId()) {
			String id = gaeData.getNameId(obj);
			entity = id == null ? 
				new Entity(kind) :
				new Entity(KeyFactory.createKey(kind,id));
		} else if (gaeData.isId()) {
			Long id = gaeData.getId(obj);
			entity = id == null ? 
				new Entity(kind) :
				new Entity(KeyFactory.createKey(kind,id));
		} else {
			Key key = gaeData.getKey(obj);
			entity = key == null ? 
				new Entity(kind) :
					(key.getName()==null && key.getId() == -1) ? new Entity(key.getKind(), key.getParent()) : new Entity(key);
		}

		return copyToEntity(entity, obj);
	}

	public static <A> F1<Entity,A> mapF(final Class<A> clazz) {
		return new F1<Entity, A>() {public A e(Entity obj) {
			return DB.map(obj, clazz);
		}};
	}

	public static <A> A map(Entity entity,Class<A> clazz) {
		
		GaeDataConverter<A> getSet = gaeConverter(clazz);
		
		return getSet.eto(entity);
	}


	public static <A> GaeDataConverter<A> gaeConverter(Class<A> clazz) {
		GaeDataConverter<A> getSet = gaeConverters.get(clazz);
		if (getSet == null) {
			getSet = AEDB.analyze(clazz);
			gaeConverters.put(clazz, getSet);
		}
		return getSet;
	}

	public static Entity copyToEntity(Entity entity, Object obj) {
		GaeDataConverter<? extends Object> getSet = gaeConverter(obj.getClass());
		getSet.ote().e(obj, entity);
		return entity;
	}

	
	public static <A> A getName(String name, Key parentKey, Class<A> clazz) {
		try {
			return map(DB.ds.get(KeyFactory.createKey(parentKey, entityType(clazz), name)), clazz);
		} catch (EntityNotFoundException e) {
			return null;
		}
	}
	
	public static <A> A getByKey(Object... keys) {
		Class stackClass = null;
		Key stackKey = null;
		for (int i = 0; i < keys.length; i++) {
			Object key = keys[i];
			if (key instanceof Key) {
				stackKey = (Key) key;
			} else if (key instanceof Class || key instanceof String) {
				String kind;
				if (key instanceof Class) {
					Class clazz = (Class) key;
					stackClass = clazz;
					kind = DB.entityType(clazz);
				} else {
					kind = (String)key;
				}
				Object id = keys[++i];
				
				stackKey = keyStack(stackKey, kind, id);
			}
		}
		return (A) DB.get(stackKey, stackClass);
	}

	public static Key key(Object... keys) {
		Key stackKey = null;
		for (int i = 0; i < keys.length; i++) {
			Object key = keys[i];
			if (key instanceof Key) {
				stackKey = (Key) key;
			} else if (key instanceof Class || key instanceof String) {
				String kind = key instanceof Class ? DB.entityType((Class) key) : (String)key;
				Object id = i < keys.length - 1 ? keys[++i] : -1L;
				
				stackKey = keyStack(stackKey, kind, id);
			}
		}
		return stackKey;
	}
	
//	public static Key keyStack(Key stackKey, Class clazz, Object oid) {
//		
//	}
	
	public static Key keyStack(Key stackKey, String kind, Object oid) {
		Long id = null;
		String name = null;
		if (oid instanceof Long) {
			id = (Long) oid;
		} else if (oid instanceof Integer) {
			id = ((Integer) oid).longValue();
		} else {
			name = (String) oid;
		}

		return stackKey == null ? 
				(id != null ? KeyFactory.createKey(kind, id) : KeyFactory.createKey(kind, name)) :
				(id != null ? KeyFactory.createKey(stackKey, kind, id) : KeyFactory.createKey(stackKey, kind, name));
	}

	public static <A> A get(Key key, Class<A> clazz) {
		try {
			return map(DB.ds.get(key), clazz);
		} catch (EntityNotFoundException e) {
			return null;
		}
	}

	public static <A> A get(Class<A> clazz, long id) {
		try {
			return map(DB.ds.get(KeyFactory.createKey(entityType(clazz), id)), clazz);
		} catch (EntityNotFoundException e) {
			return null;
		}
	}


	public static <A> A getName(String name, Class<A> clazz) {
		try {
			return map(DB.ds.get(KeyFactory.createKey(entityType(clazz), name)), clazz);
		} catch (EntityNotFoundException e) {
			return null;
		}
	}
	public static <A> void putName(Object o, String name) {
		Entity entity = new Entity(entityType(o.getClass()),name);
		copyToEntity(entity, o);
		ds.put(entity);
	}

	public static boolean existsName(Class<?> class1, String name) {
		return getName(name, class1) != null;
	}

	public static <A> List<A> getByIds(Class<A> clazz, Iterable<Long> ids) {
		String entityType = entityType(clazz);
		LinkedList<Key> keys = new LinkedList<Key>();
		for (Long id : ids) {
			keys.add(KeyFactory.createKey(entityType, id));
		}
		return getByKeys(clazz, keys);
	}
	public static <A> List<A> getByNames(Class<A> clazz, Iterable<String> names) {
		String entityType = entityType(clazz);
		LinkedList<Key> keys = new LinkedList<Key>();
		for (String id : names) {
			keys.add(KeyFactory.createKey(entityType, id));
		}
		return getByKeys(clazz, keys);
	}
	public static <A> List<A> getByKeys(Class<A> clazz, Iterable<Key> keys) {
		return Cols.yield(ds.get(keys).values(), mapF(clazz));
	}
	
	public static Key changeParentKey(Key fromParent, Key toParent, Key key) {
		return AEDB.changeParentKey(fromParent, toParent, key, new LinkedList<AEDB.KeyPart>());
	}
	
	

	public static void saveAll(List<?> objects) {
		LinkedList<Entity> entities = new LinkedList<Entity>();
		for (Object object : objects) {
			Entity entity = toEntity(object);
			entities.add(entity);
//			ds.put(entity);
		}
		ds.put(entities);
	}

	public static <A> Iterable<A> asIterable(Query q, final Class<A> clazz) {
		final Iterable<Entity> iterable = ds.prepare(q).asIterable();
		return new Iterable<A>() {

			@Override
			public Iterator<A> iterator() {
				final F1<Entity, A> mapF = mapF(clazz);
				final Iterator<Entity> iterator = iterable.iterator();
				return new Iterator<A>() {

					@Override
					public boolean hasNext() {
						return iterator.hasNext();
					}

					@Override
					public A next() {
						return mapF.e(iterator.next());
					}

					@Override
					public void remove() {
						iterator.remove();
					}
				};
			}
		};
	}

	public static <A> void each(Key id, Class<A> clazz, P1<A> p1) {
		for (A a : getIterAsync(queryByKey(clazz, id), clazz)) {
			p1.e(a);
		}
	}

	public static Iterable<Key> getKeyIter(Query query) {
		query.setKeysOnly();
		return toKeys(ds.prepare(query).asIterable());
	}

	public static void deleteChildren(Key key) {
		deleteAll(new Query(key));
	}

	public static void deleteChildren(Key key, Class<?> clazz) {
		deleteAll(query(clazz, key));
	}

	public static void delete(String id, Class<?> class1) {
		delete(id, entityType(class1));
	}

	public static void delete(Long id, Class<?> class1) {
		delete(id, entityType(class1));
	}

	public static <A> void deleteAll(Collection<A> col) {
		A single = Cols.getSingle(col);
		if (single==null) {
			// Empty
			return;
		}
		F1<A, Key> keyF = getKeyF(AEDB.analyze(single));
		List<Key> keys = Cols.yield(col, keyF);
		
		ds.delete(keys);
	}

	public static <A> void deleteAllAsync(Collection<A> col) {
		A single = Cols.getSingle(col);
		if (single==null) {
			return;
		}
		F1<A, Key> keyF = getKeyF(AEDB.analyze(single));
		List<Key> keys = Cols.yield(col, keyF);
		
		ads.delete(keys);
	}

	
	private static <A> F1<A,Key> getKeyF(final GaeDataConverter<A> analyze) {
		return new F1<A, Key>() {public Key e(A obj) {
			return analyze.getKey(obj);
		}};
	}
}
